/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * This file is part of retabel.
 *
 * Copyright (c) 2011 by individual contributors as indicated by the
 * @author tags. See the copyright.txt file in the distribution
 * for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.retabel.service.render;

import org.retabel.persistence.dao.configuration.ConfigDao;
import org.retabel.persistence.dao.document.DocumentConfigurationDao;
import org.retabel.persistence.dao.render.RenderedDocumentDao;
import org.retabel.persistence.entity.configuration.ConfigAttribute;
import org.retabel.persistence.entity.configuration.ConfigSet;
import org.retabel.persistence.entity.document.ContentTypeEnum;
import org.retabel.persistence.entity.document.DocumentConfiguration;
import org.retabel.persistence.entity.document.DocumentConfigurationTypeEnum;
import org.retabel.persistence.entity.render.RenderedDocument;
import org.retabel.persistence.entity.render.RenderedDocumentPart;
import org.retabel.persistence.entity.report.RenderingEngineEnum;
import org.retabel.service.configuration.SystemConfigAccessor;
import org.retabel.service.failover.FailoverService;
import org.retabel.service.mapper.XmlRenderDocumentConfigurationMapper;
import org.retabel.service.render.exportable.ExportableDocumentPart;
import org.retabel.service.render.exporter.RenderingEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.ejb.Asynchronous;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Logic to do the render process. Datasources and layouts are combined in this class depending on the document configuration.
 * This service runs asynchronous to prevent callers from blocking. The generated document can be found later with a generated
 * unique document number.
 * <p/>
 * <h3>Extra-Info</h3>
 * Created: 22:05 21.09.11
 *
 * @author Burt Parkers
 * @version 1.0
 */
@Stateless
public class RenderAsyncServiceBean implements RenderAsyncService {

    /** The Logger. */
    private static final Logger logger = LoggerFactory.getLogger(RenderServiceBean.class);

    /** Service to access the configuration. */
    @EJB
    private SystemConfigAccessor systemConfigAccessor;

    /** Service for Xml processing. */
    @EJB
    private XmlRenderDocumentConfigurationMapper configurationMapper;

    /** Dao to access the rendered documents. */
    @EJB
    private RenderedDocumentDao renderedDocumentDao;

    /** The failover service. */
    @EJB
    private FailoverService failoverService;

    /** Dao to access config sets. */
    @EJB
    private ConfigDao configDao;

    /** Dao to access the document configurations. */
    @EJB
    private DocumentConfigurationDao documentConfigurationDao;

    /**
     * Does the render process. This method is done asynchronous to prevent clients from blocking.
     *
     * @param renderParameter the render parameter, contains information about configuration, data and process information.
     */
    @Override
    @Asynchronous
    @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
    public void render(RenderParameter renderParameter) {
        try {
            byte[] documentConfigurationData = renderParameter.getDocumentConfigurationData();
            DocumentConfiguration documentConfiguration = configurationMapper.unmarshal(documentConfigurationData);

            if (DocumentConfigurationTypeEnum.TEMPORARY.equals(documentConfiguration.getConfigurationType())) {
                // temporary document configurations need to be persisted before rendering, so the render process can be
                // restarted when failed
               documentConfiguration = documentConfigurationDao.merge(documentConfiguration);
            }

            // test the configuration feasibility
            DocumentConfigurationValidator documentConfigurationValidator = new DocumentConfigurationValidator();
            documentConfigurationValidator.testDocumentConfigurationFeasibility(documentConfiguration);

            final byte[] xmlData = renderParameter.getXmlData();

            if (logger.isDebugEnabled()) {
                logger.debug("Size of ConfigData: " + documentConfigurationData.length + ", xmlData: " + xmlData.length);
            }

            ExportableDocumentPartSplitter exportableDocumentPartSplitter =
                new ExportableDocumentPartSplitter(systemConfigAccessor.getDesignCacheSize(), getReportDesignURI(), xmlData);

            // count equal consecutive rendering engines and group them by content type
            Map<ContentTypeEnum, List<RenderingEngineJob<ExportableDocumentPart>>> splittedDocumentConfiguration =
                exportableDocumentPartSplitter.split(documentConfiguration);

            // export it

            List<RenderedDocumentPart> renderedDocumentParts = new LinkedList<>();

            for (ContentTypeEnum contentType : splittedDocumentConfiguration.keySet()) {
                List<RenderingEngineJob<ExportableDocumentPart>> renderingEngineJobs = splittedDocumentConfiguration.get(
                    contentType);

                RenderedDocumentPart renderedDocumentPart = exportContentType(renderingEngineJobs, contentType);

                renderedDocumentParts.add(renderedDocumentPart);
            }

            // finally store the rendered documents
            storeDocument(renderParameter.getUniqueDocumentNumber(), documentConfiguration, renderedDocumentParts);

        } catch (Exception e) {
            failoverService.failover(renderParameter, RenderParameter.class.getName());
            throw new RuntimeException(e);
        }
    }

    /**
     * Renders a list of rendering engine jobs with the associated rendering engine as the given content type
     *
     * @param renderingEngineJobs the rendering engine jobs to render
     * @param contentType         the content type to be created
     * @return The rendered job as instance of {@link RenderedDocumentPart}
     * @throws RenderServiceException thrown if an exception occurs
     */
    private RenderedDocumentPart exportContentType(List<RenderingEngineJob<ExportableDocumentPart>> renderingEngineJobs,
                                                   ContentTypeEnum contentType) throws RenderServiceException {

        List<RenderedDocumentPart> renderedDocumentParts = new LinkedList<>();

        for (RenderingEngineJob<? extends ExportableDocumentPart> renderingEngineJob : renderingEngineJobs) {

            RenderingEngineEnum renderingEngineEnum = renderingEngineJob.getRenderingEngine();
            
            RenderingEngine renderingEngine = RenderEngineFactory.create(renderingEngineEnum, getReportDesignURI());
            RenderedDocumentPart renderedDocumentPart =
                renderingEngine.exportAndConcatenate(renderingEngineJob.getEntries(), contentType);

            renderedDocumentParts.add(renderedDocumentPart);
        }

        if (renderedDocumentParts.size() > 1) {
            // concatenate rendered documents
            RenderedDocumentPart renderedDocumentPart = new RenderedDocumentPart();
            renderedDocumentPart.setContentType(contentType);
            renderedDocumentPart.setContent(concatenateRenderedDocuments(contentType, renderedDocumentParts));

            return renderedDocumentPart;
        }

        // return the only entry
        if (renderedDocumentParts.size() == 1) {
            return renderedDocumentParts.get(0);
        }

        throw new RenderServiceException("nothing to do?");
    }

    /**
     * Concatenate rendered documents parts into a single rendered document part
     *
     * @param contentType           the content type to create
     * @param renderedDocumentParts the rendered document parts to concatenate
     * @return The concatenated rendered document parts as single instance of {@link RenderedDocumentPart}
     * @throws RenderServiceException thrown if an exception occurs
     */
    private byte[] concatenateRenderedDocuments(ContentTypeEnum contentType, List<RenderedDocumentPart> renderedDocumentParts)
        throws RenderServiceException {

        try {
            List<byte[]> renderedContentList = new LinkedList<>();
            for (RenderedDocumentPart renderedDocumentPart : renderedDocumentParts) {
                renderedContentList.add(renderedDocumentPart.getContent());
            }

            switch (contentType) {
                case PDF: {
                    return RenderingEngine.concatPDFs(renderedContentList);
                }
            }

            throw new RenderServiceException(
                "concatenation with different rendering engines isn't supported for content type: '" + contentType + "'");
        } catch (Exception e) {
            throw new RenderServiceException(e.getMessage(), e);
        }
    }

    /**
     * Retrieves the report design uri from config set report_server[uri]
     *
     * @return the report design base uri
     * @throws RenderServiceException if the uri property isn't a valid uri
     */
    private URI getReportDesignURI() throws RenderServiceException {
        ConfigSet configSet = configDao.findByName("report_server");
        if (configSet == null) {
            throw new RuntimeException("missing config set: report_server");
        }

        ConfigAttribute configAttribute = configSet.getConfigAttribute("uri");

        if (configAttribute == null) {
            throw new RuntimeException("missing config set attribute: report_server[uri]");
        }

        try {
            return new URI(configAttribute.getValue());
        } catch (URISyntaxException e) {
            throw new RenderServiceException(e.getMessage(), e);
        }
    }

    /**
     * Stores a document to the database.
     *
     * @param docId                 the unique document identifier to use. Necessary for the calling client to find the document.
     * @param documentConfiguration the document configuration to reference.
     * @param partList              the rendered parts of the document.
     */
    private void storeDocument(String docId, DocumentConfiguration documentConfiguration, List<RenderedDocumentPart> partList) {

        RenderedDocument renderedDocument = new RenderedDocument();
        renderedDocument.setDocumentIdentifier(docId);
        renderedDocument.setDocumentConfiguration(documentConfiguration);
        renderedDocument.setRenderedDocumentParts(partList);

        renderedDocumentDao.create(renderedDocument);
    }

}
